package com.averroes
{
	import flash.geom.Point;
	
	public class Gesture
	{
		private var _xs:Array;
		private var _ys:Array;
		private var _ts:Array;
		
		/**
		* A private variable to hold the arc length at each point for this gesture. We keep a copy so that we calculate it only once.
		*/		
		private var _arcLengths:Array;
		
		/**
		* A private variable to hold the pen speed at each point for this gesture. We keep a copy so that we calculate it only once.
		*/			
		private var _penSpeeds:Array;
		
		private var _slopes:Array;
		
		private var _tangentRegressionLineSlopes:Array;
		private var _regressionWindow:int;


		
		public function Gesture(xs:Array, ys:Array, ts:Array):void
		{
			_xs = xs;
			_ys = ys;
			_ts = ts;
		}
		
		public function get xs():Array { return _xs; }
		public function get ys():Array { return _ys; }
		public function get ts():Array { return _ts; }
		
		/**
		 * The number of points in a given gesture.
		 * @return 
		 * 
		 */		
		public function get length():int
		{
			return xs.length;
		}
		
		/**
		 * Converts this gesture to an array of points 
		 * @return 
		 * 
		 */		
		public function toPoints():Array
		{
			//instantiate the return array to an appropriate length.
			var ret:Array = new Array(_xs.length);
			for (var i:int=0; i<_xs.length; i++)
				ret[i] = new Point(_xs[i], _ys[i]);
			return ret;
		}
		
		/**
		 * The length of the arc at each point in this gesture. 
		 * @return 
		 * 
		 */		
		public function get arcLengths():Array
		{
			if (null == _arcLengths)
				_arcLengths = __calculateArcLengths();
			return _arcLengths;
		}
		
		/**
		 * The speed of the pen at each point in this gesture
		 * @return 
		 * 
		 */		
		public function get penSpeeds():Array
		{
			if (null == _penSpeeds)
				_penSpeeds = __calculatePenSpeeds();
			return _penSpeeds;
		}
		
		/**
		 * Returns the line slopes 
		 * @return 
		 * 
		 */		
		public function get slopes():Array
		{
			if (null == _slopes)
				_slopes = __calculateSlopes();
			return _slopes;
		}
		
		/**
		 * Calculates the slope of the tangent at each given point.
		 * @param window 
		 * @return an array of tangent slopes at each point.
		 * 
		 */		
		public function getTangentSlopes(window:int):Array
		{
			if (window != _regressionWindow || null == _tangentRegressionLineSlopes)
			{
				_regressionWindow = window;
				_tangentRegressionLineSlopes = __calculateRegressionLineSlopes(_regressionWindow);
			}
			return _tangentRegressionLineSlopes;		
		}	
		
		private function __calculateRegressionLineSlopes(window:int):Array
		{
			var ret:Array = new Array();
			for (var i:int=0; i<xs.length; i++)
			{
				var upper:int = Math.max(1, i-Math.floor(window/2));
				var lower:int = Math.min(xs.length-1, i+Math.floor(window/2));
				var regress:Object = Statistics.regress(ys.slice(upper, lower), xs.slice(upper,lower));
				ret.push(regress.m)
			}
			return ret;	
		}
		
		private function __calculateSlopes():Array
		{
			var ret:Array = new Array();
			for (var i:int=0; i<length-1; i++)
			{
				ret.push(Geometry.slope(new Point(xs[i], ys[i]), new Point(xs[i+1], ys[i+1])));
			}
			return ret;
		}	
		
		/**
		 * 
		 * @return 
		 * @private
		 */		
		private function __calculateArcLengths():Array
		{
			var ret:Array = new Array();
			var distance:int = 0;
			
			ret[0] = 0;
			for (var n:int=1; n<length; n++)
			{
				//aggregate euclidean distance		
				distance += Point.distance(new Point(xs[n], ys[n]), new Point(xs[n-1], ys[n-1]));
				ret[n] = distance;
			}
			return ret;
		}
		
		/**
		 *         %Pen speed
        S(j,1) = (D(j+1) - D(j-1))/((stroke.t(j+1))-(stroke.t(j-1)));
		 * @return 
		 * 
		 */		
		private function __calculatePenSpeeds():Array
		{
			var ret:Array = new Array();
			for (var n:int=1; n<length-1; n++)
			{
				//calculate time delta, to make sure we don't divide by zero.
				var dt:Number = ts[n+1] - ts[n-1];
				if (dt > 0)
					ret[n] = (arcLengths[n+1] - arcLengths[n-1])/dt;
				else
					ret[n] = 0;
			}
			
			//handle the initial and final pen speeds
			if (length > 0)
				ret[0] = ret[1];
			ret[ret.length] = ret[ret.length-1];
			return ret;
		}
		
		/**
		 * Calculates curvature at every point in a gesture.
		 * @param window
		 * @return 
		 * 
		 */		
		public function calculateCurvature(window:int):Array
		{
			var curvature:Array = new Array();
			
			//slopes
			var slopes:Array = getTangentSlopes(window);
			//angles in radians
			var angles:Array = new Array();
			//correct the angles and convert them to degrees
			for (var i:int=0; i<slopes.length; i++)
				angles.push(Math.atan(slopes[i])*180/Math.PI);
			var correctAngles:Array = correctAngleCurve(angles);
			
			var dAngles:Array = Calculus.diff(correctAngles);
			var dArcs:Array = Calculus.diff(arcLengths);
			for (i=0; i<dAngles.length; i++)
				curvature.push(dAngles[i]/(dArcs[0] | 1));
			return curvature;
		}
		/**
		 * output = correctAngleCurve(angles)
		 * corrects discontinuities in a sequence of angles by "rolling out" the
		 * unit circle
		 * @param angles
		 * @return 
		 * 
		 */		
		private function correctAngleCurve(angles:Array):Array
		{
			var adjust:int = 0;
			var ret:Array = new Array();
			ret[0] = angles[0];
			for (var i:int=0; i<angles.length; i++)
			{
				if (Math.abs(angles[i]-angles[i-1]) > Math.abs(angles[i]-Math.PI-angles[i-1]))
					adjust -= Math.PI;
				else if (Math.abs(angles[i]-angles[i-1]) > Math.abs(angles[i]+Math.PI-angles[i-1]))
					adjust += Math.PI;
				ret[i] = angles[i] + adjust;
			}
			return ret;
		}
	
		
		/**
		 * Exports the gesture to a matlab structure.
		 * @return 
		 * 
		 */		
		public function ToMFile():String
		{
			var xstr:String = "[" + xs.join(" ") + "]'";
			var ystr:String = "[" + ys.join(" ") + "]'";
			var tstr:String = "[" + ts.join(" ") + "]'";
			var stroke:String = "struct(";
			stroke += "'x', {" + xstr +  "},";
			stroke += "'y', {" + ystr +  "},";
			stroke += "'t', {" + tstr +  "}";
			stroke += ")";
			return stroke;			
		}		
	}
}